package org.zyzboole.base.encrypt.core;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.eyelake.common.core.exception.BizException;
import lombok.Data;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.JdbcParameter;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.LikeExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.select.*;
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.util.deparser.ExpressionDeParser;
import org.zyzboole.base.encrypt.rewrite.condition.EncryptCondition;
import org.zyzboole.base.encrypt.rule.EncryptRule;
import org.zyzboole.base.encrypt.util.TablesNamesFinderPlus;

import java.util.*;

/**
 * @author liangwx
 */
@Data
public class CCJSQLStatementContext {

    private Statement statements;

    private List<CCJSQLTableDTO> tableList;

    private final EncryptRule encryptRule;

    private String sql;

    public CCJSQLStatementContext(String sql, EncryptRule encryptRule) throws JSQLParserException {

        this.sql = sql;
        this.encryptRule = encryptRule;
        try {
            Statement statements = CCJSqlParserUtil.parse(sql);
            this.statements = statements;
            TablesNamesFinderPlus tablesNamesFinderPlus = new TablesNamesFinderPlus();
            tableList = tablesNamesFinderPlus.getTableList(statements);
        } catch (Exception e) {

        }
    }

    private List<CCJSQLCryptExpressionDTO> parseInsert(Insert insert) {
        List<Column> columns = insert.getColumns();
        List<CCJSQLCryptExpressionDTO> result = new ArrayList<>();
        for (int i = 0; i < columns.size(); i++) {
            Column column = columns.get(i);
            CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
            dto.setAlias(TablesNamesFinderPlus.getAlias(column.getTable()));
            dto.setColumnName(column.getColumnName());
            dto.setIndex(i + 1);
            result.add(dto);
        }
        return result;
    }

    private List<CCJSQLCryptExpressionDTO> parseUpdate(Update update) {
        List<Column> columns = update.getColumns();
        List<CCJSQLCryptExpressionDTO> result = new ArrayList<>();
        for (int i = 0; i < columns.size(); i++) {
            Column column = columns.get(i);
            CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
            dto.setAlias(TablesNamesFinderPlus.getAlias(column.getTable()));
            dto.setColumnName(column.getColumnName());
            dto.setIndex(i + 1);
            result.add(dto);
        }
        return result;
    }

    public List<EncryptCondition> createEncryptConditions() {

        List<CCJSQLCryptExpressionDTO> list = null;

        if (statements instanceof Select) {
            Select select = (Select) statements;
            list = parseSelect(select);
        } else if (statements instanceof Insert) {
            Insert insert = (Insert) statements;
            list = parseInsert(insert);
        } else if (statements instanceof Update) {
            Update update = (Update) statements;
            list = parseUpdate(update);
        }

        if (CollectionUtil.isEmpty(list)) {
            return Collections.emptyList();
        }

        List<EncryptCondition> result = new LinkedList<>();
        for (CCJSQLCryptExpressionDTO dto : list) {
            result.addAll(createEncryptConditions(dto));
        }

        return result;
    }


    public List<CCJSQLCryptExpressionDTO> parseSelect(Select select) {

        StringBuilder b = new StringBuilder();

        List<CCJSQLCryptExpressionDTO> list = new ArrayList<>();
        parseSelectBody(list, b, select.getSelectBody());
        return list;
    }

    /**
     * 查询解析
     *
     * @param list
     * @param b
     * @param selectBody
     */
    public void parseSelectBody(List<CCJSQLCryptExpressionDTO> list, StringBuilder b, SelectBody selectBody) {
        if (selectBody instanceof PlainSelect) {
            parseSelectOneObject(list, b, selectBody);
        } else if (selectBody instanceof SetOperationList) {
            SetOperationList setOperationList = (SetOperationList) selectBody;
            List<SelectBody> selects = setOperationList.getSelects();
            if (!Objects.isNull(selects)) {
                selects.forEach(item -> {
                    parseSelectOneObject(list, b, item);
                });
            }
        }
    }

    /**
     * 解析单SelectBody
     *
     * @param list
     * @param b
     * @param selectBody
     */
    public void parseSelectOneObject(List<CCJSQLCryptExpressionDTO> list, StringBuilder b, SelectBody selectBody) {
        PlainSelect plain = (PlainSelect) selectBody;
        // 针对 临时表问题
        FromItem fromItem = plain.getFromItem();
        if (fromItem instanceof SubSelect) {
            SubSelect subSelect = (SubSelect) fromItem;
            parseSelectBody(list, b, subSelect.getSelectBody());
        }
        Expression where = plain.getWhere();
        if (Objects.isNull(where)) {
            return;
        }
        where.accept(new ExpressionDeParser(null, b) {
            @Override
            public void visit(EqualsTo equalsTo) {
                expression(list, equalsTo);
            }

            @Override
            public void visit(LikeExpression likeExpression) {
                expression(list, likeExpression);
            }
        });
    }

    /**
     * 参数解析
     *
     * @param list
     * @param binaryExpression
     */
    private void expression(List<CCJSQLCryptExpressionDTO> list, BinaryExpression binaryExpression) {
        Expression left = binaryExpression.getLeftExpression();
        if (left instanceof Column) {
            Column leftExpression = (Column) binaryExpression.getLeftExpression();
            Expression rightExpression = binaryExpression.getRightExpression();
            if (rightExpression instanceof JdbcParameter) {
                JdbcParameter jdbcParameter = (JdbcParameter) rightExpression;
                Integer index = jdbcParameter.getIndex();
                CCJSQLCryptExpressionDTO dto = new CCJSQLCryptExpressionDTO();
                dto.setAlias(TablesNamesFinderPlus.getAlias(leftExpression.getTable()));
                dto.setColumnName(leftExpression.getColumnName());
                dto.setIndex(index);
                list.add(dto);
            }
        }
    }


    private Collection<EncryptCondition> createEncryptConditions(CCJSQLCryptExpressionDTO dto) {
        Collection<EncryptCondition> result = new LinkedList<>();
        createEncryptCondition(dto).ifPresent(result::add);
        return result;
    }

    private Optional<EncryptCondition> createEncryptCondition(CCJSQLCryptExpressionDTO dto) {
        Optional<String> tableName = findTableName(dto);
        if (!tableName.isPresent()) {
            return Optional.empty();
        }
        final EncryptCondition condition = new EncryptCondition(tableName.get(), dto.getColumnName(), dto.getIndex());
        return tableName.isPresent() && containsColumn(tableName.get(), dto.getColumnName())
                ? Optional.of(condition) : Optional.empty();
    }

    public Optional<String> findTableName(CCJSQLCryptExpressionDTO dto) {

        if (tableList.size() == 1) {
            return Optional.of(CollectionUtil.getFirst(tableList).getTableName());
        } else if (StrUtil.isNotEmpty(dto.getColumnName())) {
            return findTableNameFromMetaData(dto.getColumnName());
        }
        return Optional.of(findTableNameFromSQL(dto.getAlias()));
    }

    private Optional<String> findTableNameFromMetaData(final String columnName) {
        for (CCJSQLTableDTO each : tableList) {
            if (containsColumn(each.getTableName(), columnName)) {
                return Optional.of(each.getTableName());
            }
        }
        return Optional.empty();
    }

    private boolean containsColumn(String tableName, String columnName) {
        return encryptRule.containsColumn(tableName, columnName);
    }

    private String findTableNameFromSQL(final String tableNameOrAlias) {
        for (CCJSQLTableDTO each : tableList) {
            if (tableNameOrAlias.equalsIgnoreCase(each.getTableName()) || tableNameOrAlias.equals(each.getAlisaName())) {
                return each.getTableName();
            }
        }
        throw BizException.getInstance("Can not find owner from table.");
    }
}
